package game.server;

import game.Card;
import game.Deck;
import game.Player;
import game.client.Table;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.concurrent.Semaphore;


import server.ActiveSessions;
import server.ClientSession;
import server.Message;
import server.OutboundMessages;
import server.Server;
import server.utils.CommandType;
import server.utils.PlayerWatcher;
import server.utils.Utils;

public class Game extends Thread{
	/**
	 * Server's sleeptime to give clients some time to sync up.
	 */
	private final int SLEEP = 1000;
	private ActiveSessions activeSessions;
	private OutboundMessages outMessages;
	private OutboundMessages inMessages;
	private PlayerWatcher playerWatcher;
	private Latch startLatch;
	/**
	 * True if game is running.
	 * False if waiting for players.
	 */
	private boolean running;
	private Object runningSync;
	private Semaphore maxPlayers;
	private Deck deck;
	private ServerTable table;
	private ArrayList<Player> usedPlayers;
	private EvalHand evalHand;
	/**
	 * Initializes new game.
	 * Also makes new Latch(Server.conf.getMinPlayers()).
	 * Makes new PlayerWatcher.
	 * Makes new Semaphore for maxPlayers.
	 * Starts thread.
	 * @param activeSessions
	 * @param outMessages
	 * @param inMessages
	 */
	public Game(ActiveSessions activeSessions, OutboundMessages outMessages,
			OutboundMessages inMessages) {
		super("Game");
		this.running = false;
		this.activeSessions = activeSessions;
		this.outMessages = outMessages;
		this.inMessages = inMessages;
		this.runningSync = new Object();
		this.playerWatcher = new PlayerWatcher(Server.conf.getMinPlayers(),
				activeSessions, this);
		this.maxPlayers = new Semaphore(Server.conf.getMaxPlayers());
		this.table = new ServerTable(this);
		this.startLatch = new Latch(Server.conf.getMinPlayers(), table);
		this.evalHand = new EvalHand();
		start();
	}

	@Override
	public void run() {
		try {
			startLatch.await(); //wait until there is enough players
			outMessages.addMessage(new Message(this.getName(), 
					CommandType.INFO, "Players ready, game started..."));
		} catch (InterruptedException e) {
			Server.log.println("Interrupted error @ " +
					"Thread game --> starting latch", Server.conf.isConsole());
		} finally {
			startLatch.countDown(); //notify all
			this.setRunning(true);
		}
		while(true) {
			if (this.isRunning()) { //game life cycle
				//game cycle
				deck = new Deck(); //also shuffle
				dealStartingCards(deck);
				getBets();
				dealCardToTable(Server.conf.getFirstCardsTable(), deck);
				getBets();
				for (int i = 0; i < Server.conf.getRoundLimit() - 1; i++) { //def 2x
					dealCardToTable(Server.conf.getCardsDealedRound(), deck);
					getBets();
				}
				
				ArrayList<Player> winners = getWinners();
				if (winners.size() > 0) {
					dealMoney(winners); //also take cards back from players
				}
				table.clearBetsOnTable();
				table.clearCardsOnTable();
				Player player;
				usedPlayers = new ArrayList<Player>();
				while((player = getNextPlayer(usedPlayers)) != null) {
					player.clearCards();
				}
				outMessages.addMessage(new Message(Server.conf.getServerName(),
						CommandType.ENDOFROUND, "End of round."));
				// send all nessesary data
				table.emptyWaitlistBuffer();
				playerWatcher.checkPlayerCount();
				outMessages.addMessage(new Message(Server.conf.getServerName(),
						CommandType.SENDALLDATA, Utils.convertServerTable(table)));
				try {
					//sleep to wait for clients to sync up
					sleep(SLEEP);
				} catch (InterruptedException e) {
					Server.log.println("Sleeping error @ Game.run()", 
							Server.conf.isConsole());
				}
			} else {
				try {
					playerWatcher.waitForPlayers();
					table.emptyWaitlistBuffer();
					setRunning(true);
				} catch (InterruptedException e) {
					Server.log.println("Interrupted error @ thread Game --> " +
							"waiting for playerwatcher", Server.conf.isConsole());
				}
			}
		}
		
	}
	/**
	 * Gets winners.
	 * @return Winners
	 */
	private ArrayList<Player> getWinners() {
		ArrayList<Player> output = new ArrayList<Player>();
		Player player;
		usedPlayers = new ArrayList<Player>();
		ArrayList<Card> tableCards = table.getAllCardsOnTable();
		int curMaxHand = 0;
		ArrayList<Player> temp = table.getPlayingPlayers();
		if (temp.size() < Server.conf.getMinPlayers()) {
			return temp;
		}
		while((player = getNextPlayer(usedPlayers)) != null) {
			ArrayList<Card> tmp = new ArrayList<Card>();
			tmp.addAll(tableCards);
			tmp.addAll(player.getCards());
			int hand = evalHand.evaluate(tmp);
			if (hand == curMaxHand) {
				output.add(player);
			} else if (hand > curMaxHand) {
				output.removeAll(output);
				curMaxHand = hand;
				output.add(player);
			}
		}
		return output;
	}
	/**
	 * Deal money to winners.
	 * Takes all cards from players. Clears bets on table.
	 * @param winners Players to deal money
	 */
	private void dealMoney(ArrayList<Player> winners) {
		if (winners.size() > 0) {
			double moneyToDeal = (double) table.getBetOnTable() / 
			(double) winners.size();
			for (Player s : winners) { //go through all winners
				String name = s.getName();
				outMessages.addMessage(new Message(Server.conf.getServerName(),
						name, CommandType.DEALMONEY, String.valueOf(moneyToDeal)));
				outMessages.addMessage(new Message(name,
						CommandType.DEALMONEY, String.valueOf(moneyToDeal)));
				waitResponseOK(name);
				usedPlayers = new ArrayList<Player>();
				Player player;
				while((player = getNextPlayer(usedPlayers)) != null) {
					if (player.getName().equalsIgnoreCase(name)) {
						player.addMoney(moneyToDeal);
					}
				}
			}
		}
	}
	/**
	 * Get all clients bets.
	 */
	private void getBets() {
		Player player;
		double maxBet = 0;
		int n = 0;
		int count;
		do {
			usedPlayers = new ArrayList<Player>();
			while((player = getNextPlayer(usedPlayers)) != null) {
				String name = player.getName();
				setClientWaitTurn(name, false);
				if (!waitResponseOK(name)) {
					continue; //client died
				}
				Message msg;
				if ((msg = waitResponse(name, CommandType.BET)) == null) {
					continue; //client died
				}
				setClientWaitTurn(name, true);
				if (!waitResponseOK(name)) {
					continue; //client died
				}
				double bet = Double.parseDouble(msg.getCommandValue());
				if (bet < 0) {
					table.foldPlayer(name);
					continue;
				} else if (bet > 0) {
					player.bet(bet);
					table.addBetToTable(bet);
				}
				if (player.getCurrentBet() > maxBet) {
					n = 1;
					maxBet = player.getCurrentBet();
				} else {
					n++;					
				}
			}
			count = table.getPlayersCount();
			if (count < Server.conf.getMinPlayers()) {
				n = count;
			}
		} while(n < count);
	}
	/**
	 * Sets clients waiting turns.
	 * @param name Client whose wait to set.
	 * @param state State to put client in.
	 * @return True if client's turn was changed succesfully.
	 * 		   False if client died.
	 */
	private void setClientWaitTurn(String name, boolean state) {
		synchronized (activeSessions) {
			Iterator<ClientSession> active = activeSessions.iterator();
			while(active.hasNext()) {
				ClientSession cli = active.next();
				if (cli.isAlive()) {
					if (cli.getName().equalsIgnoreCase(name)) {
						cli.setWaitTurn(state);
						break;
					}
				} else {
					active.remove();
				}
			}
		}
	}
	/**
	 * Deal cards on table. 
	 * @param times How many cards to deal
	 * @param deck From where to deal
	 */
	private void dealCardToTable(int times, Deck deck) {
		for (int i = 0; i < times; i++) {
			Card card = deck.takeCard();
			table.addCard(card);
			usedPlayers = new ArrayList<Player>();
			Player player;
			while((player = getNextPlayer(usedPlayers)) != null) {
				outMessages.addMessage(new Message(Server.conf.getServerName(),
						player.getName(), CommandType.DEALTABLE, card));
				if (!waitResponseOK(player.getName())) {
					continue; //client died
				}
			}
		}
	}
	/**
	 * Deals hands to all players.
	 * @param deck
	 */
	private void dealStartingCards(Deck deck) {
		usedPlayers = new ArrayList<Player>();
		Player player;
		while((player = getNextPlayer(usedPlayers)) != null) { //also add to usedPlayers
			for (int i = 0; i < Server.conf.getMaxPlayerHand(); i++) {
				Card card = deck.takeCard();
				player.dealCard(card);
				outMessages.addMessage(new Message(Server.conf.getServerName(),
						player.getName(), CommandType.SENDCARD, card));
				outMessages.addMessage(new Message(player.getName(), 
						CommandType.SENDCARD, new Card("X", 1)));
				if (!waitResponseOK(player.getName())) {
					break; //client died
				}
			}
		}
	}
	/**
	 * Gets next unused player from list.
	 * Adds player to usedPlayers list.
	 * @param usedPlayers Players that are already used.
	 * @return Unused player. Null if all players are used.
	 */
	private Player getNextPlayer(ArrayList<Player> usedPlayers) {
		int count = table.getPlayersCount();
		if (count < Server.conf.getMinPlayers()) {
			return null;
		} else {
			synchronized (table) {
				Iterator<Player> players = table.playersIterator();
				Player player;
				while(players.hasNext()) {
					player = players.next();
					if (usedPlayers.contains(player)) {
						continue;
					} else {
						usedPlayers.add(player);
						return player;
					}
				}
				return null;
			}
		}
	}
	/**
	 * Waits OK message from client.Waits until correct message
	 * is recieved or client is dead.
	 * Contains while(true) cycle.
	 * @param name Client who should send message
	 * @return True if client is alive and conformation recieved.
	 * 		   False if client is dead.
	 */
	private boolean waitResponseOK(String name) {
		if (waitResponse(name, CommandType.OK) != null) {
			return true;
		} else {
			return false;
		}
	}
	/**
	 * Waits desired command from client. Waits until correct message
	 * is recieved or client is dead.
	 * Sends CommandType.ERROR if wrong message is recieved.
	 * Contains while(true) cycle.
	 * @param name Client who should send that message
	 * @param type Command type to wait
	 * @return Message if message type and sender are correct.
	 * 		   NULL if message is null object and client has left.
	 */
	private Message waitResponse(String name, CommandType type) {
		while(true) {
			Message msg = inMessages.getMessage(); //wait
			if (!msg.getType().equals(CommandType.QUIT)) {
				if (msg.getFrom().equalsIgnoreCase(name) && 
						msg.getType().equals(type)) {
					return msg;
				} else {
					outMessages.addMessage(
							new Message(Server.conf.getServerName(),
									msg.getFrom(), CommandType.ERROR, 
							"Wrong command or wrong client!"));
				}
			} else {
				if (msg.getFrom().equalsIgnoreCase(name)) {
					break;
				}
			}
		}
		return null; //client died
	}
	/**
	 * @return the startLatch
	 */
	public synchronized Latch getStartLatch() {
		return startLatch;
	}

	/**
	 * True - game runs.
	 * False - game is paused.
	 * @return Game run state.
	 */
	public boolean isRunning() {
		synchronized (runningSync) {
			return running;
		}
	}

	/**
	 * True - game runs.
	 * False - game is paused.
	 * @param running Set game to run or pause.
	 */
	public void setRunning(boolean running) {
		synchronized (runningSync) {
			this.running = running;
		}
	}

	/**
	 * @return the playerWatcher
	 */
	public synchronized PlayerWatcher getPlayerWatcher() {
		return playerWatcher;
	}

	/**
	 * @return the maxPlayers
	 */
	public synchronized Semaphore getMaxPlayers() {
		return maxPlayers;
	}

	/**
	 * @return the table
	 */
	public synchronized ServerTable getTable() {
		return table;
	}
	
}
